﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Diagnostics;
using System.Xml;
using System.Windows.Forms;

namespace SolutionCrapFinder
{
    enum ProjectLanguage
    {
        Unknown,
        CSharp,
        CPP,
        VB,
    }
    class ProjectInfo
    {
        public string projectName = "";
        string projectPath = "";
        public string projectGuid = "";
        public ProjectLanguage projectLang = ProjectLanguage.Unknown;
        public HashSet<string> projectRelPaths = new HashSet<string>();        
        public StringBuilder errorReport = new StringBuilder();

        public HashSet<string> wrongIncludePaths = new HashSet<string>();

        public string ProjectPath
        {
            get { return projectPath; }
            set { projectPath = value.ToLower(); }
        }
    }
    enum FileUseType
    {
        Undefined,
        Include,
        Import,
    }

    class SolutionItem
    {
        // members
        string m_slnMainFile;
        string m_slnDirectory;
        List<ProjectInfo> m_projects = new List<ProjectInfo>();
        List<string> m_allFiles = new List<string>();
        List<SolutionFile> m_slnFiles = new List<SolutionFile>();
        List<string> m_ignoredFiles = new List<string>();

        string[] m_FiltersContains = new string[] {
            @"\commonaddin\addinsrc\build\generated",
            @"\commonaddin\addinsrc\output",
            @"copyarea.db",
            @"\ATLServer",
            @"\WTL80",
            @"\BIRepository\BIPlus_Connector\Web References",
            @"\addinsrc\build\debug",
            @"\_crap"
        };

        string[] m_FiltersEndsWith = new string[]{
            ".dll"
        };

        public SolutionItem(string solutionFile, string solutionDir)
        {
            m_slnMainFile = solutionFile;
            m_slnDirectory = solutionDir;
        }

        public void SetIgnoredItems(List<string> files, List<string> dirs)
        {
            if (files != null)
            {
                m_ignoredFiles = files;
                Miscellaneous.ToLower(m_ignoredFiles);
            }
            if (dirs != null)
            {
                //TODO
            }
        }

        public void ProcessSolution()
        {
            _FindAllDirectoryFiles();
            _ApplyFilters();
            _ExcludeSolutionFile();
            _ParseSolutionFile();
            _ParseProjectFiles();
            _ExcludeProjectFiles();
            _ParseProjectFilesContent();
            ExcludeAllUsedFiles();
            ExcludeIgnoredItems();
        }

        public List<string> GetCrapFiles()
        {
            return m_allFiles;
        }

        public List<ProjectInfo> GetProjects()
        {
            return m_projects;
        }

        SolutionFile _AddSolutionFile(string file, out bool alreadyExists)
        {
            alreadyExists = false;

            foreach (SolutionFile f in m_slnFiles)
            {
                if (f.FileName == file)
                {
                    alreadyExists = true;
                    return f;
                }
            }

            SolutionFile fNew = new SolutionFile(file.ToLower());
            m_slnFiles.Add(fNew);
            return fNew;            
        }

        void _ParseSolutionFile()
        {
            if (!File.Exists(m_slnMainFile))
            {
                throw new Exception("File doesn't exist.");
            }

            using (StreamReader sr = new StreamReader(m_slnMainFile))
            {
                string solutionPath = PathsHelper.RetrievePath(m_slnMainFile);

                string line;
                while ((line = sr.ReadLine()) != null)
                {
                    line = line.Trim();
                    if (line.StartsWith("Project"))
                    {
                        ProjectInfo projInfo = new ProjectInfo();
                        if (!_ParseProjectString(line, ref projInfo))
                            continue;

                        projInfo.ProjectPath = PathsHelper.MakeFullPathFromRelative(
                            solutionPath,
                            projInfo.ProjectPath);

                        if (projInfo.ProjectPath.EndsWith(".vcxproj"))
                        {
                            projInfo.projectLang = ProjectLanguage.CPP;
                        }
                        else if (projInfo.ProjectPath.EndsWith(".csproj"))
                        {
                            projInfo.projectLang = ProjectLanguage.CSharp;
                        }
                        else if (projInfo.ProjectPath.EndsWith(".vbproj"))
                        {
                            projInfo.projectLang = ProjectLanguage.VB;
                        }
                        else
                        {
                            continue;
                        }

                        // insert project into the list
                        m_projects.Add(projInfo);
                    }
                }
            }
        }

        void _FindAllDirectoryFiles()
        {
            m_allFiles.AddRange(Directory.EnumerateFiles(m_slnDirectory, "*.*", SearchOption.AllDirectories));
            Miscellaneous.ToLower(m_allFiles);
        }

        void _ApplyFilters()
        {
            for (int i = m_allFiles.Count - 1; i >= 0; --i)
            {
                bool isRemoved = false;
                // "Contains" filters
                foreach (string currFilter in m_FiltersContains)
                {
                    if (m_allFiles[i].Contains(currFilter.ToLower()))
                    {
                        m_allFiles.RemoveAt(i);
                        isRemoved = true;
                        break;
                    }
                }
                if (isRemoved)
                    continue;

                // "EndsWith" filters
                foreach (string currFilter in m_FiltersEndsWith)
                {
                    if (m_allFiles[i].EndsWith(currFilter.ToLower()))
                    {
                        m_allFiles.RemoveAt(i);
                        break;
                    }
                }
            }
        }

        void _ParseProjectFiles()
        {
            foreach (ProjectInfo proj in m_projects)
            {
                switch (proj.projectLang)
                {
                case ProjectLanguage.CPP:
                    _ParseProjectCPP(proj);
                	break;
                case ProjectLanguage.CSharp:
                    _ParseProjectCSharp(proj);
                	break;
                default:
                    break;
                }
            }
        }

        void _ParseProjectCSharp(ProjectInfo projInfo)
        {
            string projPath = PathsHelper.RetrievePath(projInfo.ProjectPath);

            List<string> nonExistFiles = new List<string>();
            XmlPathStack elemStack = new XmlPathStack();

            var reader = new System.Xml.XmlTextReader(projInfo.ProjectPath);
            while (reader.Read())
            {
                if (reader.NodeType == XmlNodeType.EndElement)
                {
                    string prevElem = elemStack.Pop();
                    Debug.Assert(prevElem == reader.Name);
                    continue;
                }

                if (reader.NodeType != XmlNodeType.Element)
                    continue;

                if (!reader.IsEmptyElement)
                {
                    elemStack.Push(reader.Name);
                }                

                bool isNodeNameGood =
                    reader.LocalName == "Compile" ||
                    reader.LocalName == "EmbeddedResource" ||
                    reader.LocalName == "None" ||
                    reader.LocalName == "Content";
                if (reader.AttributeCount != 0 && isNodeNameGood)
                {
                    string includeAttr = reader.GetAttribute("Include");
                    Debug.Assert(includeAttr != null);
                    string fileName = PathsHelper.MakeFullPathFromRelative(projPath, includeAttr);
                    bool res = m_allFiles.Remove(fileName.ToLower());
                    if (!res)
                    {
                        nonExistFiles.Add(fileName);
                    }
                }
            }
            reader.Close();
        }

        void _ParseProjectCPP(ProjectInfo projInfo)
        {
            string projPath = PathsHelper.RetrievePath(projInfo.ProjectPath);

            List<string> nonExistFiles = new List<string>();
            XmlPathStack elemStack = new XmlPathStack();

            var reader = new System.Xml.XmlTextReader(projInfo.ProjectPath);
            while (reader.Read())
            {
                if (reader.NodeType == XmlNodeType.EndElement)
                {
                    string prevElem = elemStack.Pop();
                    Debug.Assert(prevElem == reader.Name);
                    continue;
                }
                //if (reader.NodeType != XmlNodeType.Element)
                //    continue;

                if (reader.NodeType == XmlNodeType.Element && !reader.IsEmptyElement)
                {
                    elemStack.Push(reader.Name);
                }                

                if (reader.NodeType == XmlNodeType.Text && 
                    elemStack.GetElemByLevelReverse(0) == "AdditionalIncludeDirectories" &&
                    elemStack.GetElemByLevelReverse(1) == "ClCompile")
                {
                    _AddIncludePath(projInfo, reader.Value);
                }

                if (reader.NodeType == XmlNodeType.Text &&
                    elemStack.GetElemByLevelReverse(0) == "IncludePath")
                {
                    _AddIncludePath(projInfo, reader.Value);
                }

                if (reader.NodeType == XmlNodeType.Element)
                {
                    bool isNodeNameGood =
                        reader.LocalName == "ClCompile" ||
                        reader.LocalName == "ClInclude" ||
                        reader.LocalName == "Midl" ||
                        reader.LocalName == "ResourceCompile" ||
                        reader.LocalName == "CustomBuild" ||
                        reader.LocalName == "None";
                    if (reader.AttributeCount != 0 && isNodeNameGood)
                    {
                        string includeAttr = reader.GetAttribute("Include");
                        Debug.Assert(includeAttr != null);
                        string fileName = PathsHelper.MakeFullPathFromRelative(projPath, includeAttr);
                        bool res = m_allFiles.Remove(fileName.ToLower());
                        if (!res)
                        {
                            nonExistFiles.Add(fileName);
                        }
                    }
                }
            }
            reader.Close();
        }

        void _AddIncludePath(ProjectInfo proj, string paths)
        {
            string projPath = PathsHelper.RetrievePath(proj.ProjectPath);
            foreach (string s in Miscellaneous.GetSplittedLines(paths, ';'))
            {
                string additionalPath = PathsHelper.MakeFullPathFromRelative(projPath, s);
                if (Directory.Exists(additionalPath))
                {
                    proj.projectRelPaths.Add(additionalPath);
                }
                else
                {
                    if (proj.wrongIncludePaths.Add(s))
                    {
                        if (s.StartsWith("%") || s.StartsWith("$"))
                        {
                            // TODO
                        }
                        else
                        {
                            proj.errorReport.AppendLine(String.Format("Unresolved include path: {0}", s));
                        }
                    }
                }
            }
        }

        void _ExcludeSolutionFile()
        {
            bool res = m_allFiles.Remove(m_slnMainFile.ToLower());
            Debug.Assert(res);
        }

        void _ExcludeProjectFiles()
        {
            foreach (ProjectInfo proj in m_projects)
            {
                bool res = m_allFiles.Remove(proj.ProjectPath.ToLower());
                Debug.Assert(res);

                // remove other project files
                m_allFiles.Remove(proj.ProjectPath + ".vspscc");
                m_allFiles.Remove(proj.ProjectPath + ".user");
                m_allFiles.Remove(proj.ProjectPath + ".filters");

                PathClipper projPathClipper = new PathClipper(proj.ProjectPath);
                string projFileName = projPathClipper.ClipRight();
                string projPath = projPathClipper.GetPath();
                string projFileNameWithoutExt = projFileName.Substring(0, projFileName.IndexOf('.'));

                string fileWithoutExt = projPath + projFileNameWithoutExt;
                m_allFiles.Remove(fileWithoutExt + ".vcproj");
                m_allFiles.Remove(fileWithoutExt + ".vcproj.vspscc");
            }
        }

        void _ParseProjectFilesContent()
        {
            foreach (ProjectInfo proj in m_projects)
            {
                if (proj.projectLang != ProjectLanguage.CPP)
                    continue;

                string projPath = PathsHelper.RetrievePath(proj.ProjectPath);
                var reader = new System.Xml.XmlTextReader(proj.ProjectPath);
                while (reader.Read())
                {
                    if (reader.NodeType != XmlNodeType.Element)
                        continue;

                    bool isNodeNameGood =
                        reader.LocalName == "ClCompile" ||
                        reader.LocalName == "ClInclude" ||
                        reader.LocalName == "None";
                    if (reader.AttributeCount != 0 && isNodeNameGood)
                    {
                        string includeAttr = reader.GetAttribute("Include");
                        Debug.Assert(includeAttr != null);
                        
                        string fileName = PathsHelper.MakeFullPathFromRelative(projPath, includeAttr);
                        string extension = Path.GetExtension(fileName);
                        if (!File.Exists(fileName))
                        {
                            proj.errorReport.AppendLine(String.Format("File included in project not found: {0}", includeAttr));
                            continue;
                        }

                        bool exists = false;
                        SolutionFile f = _AddSolutionFile(fileName, out exists);
                        f.AddProjectThatUseIt(proj);

                        if (extension != ".h" && extension != ".cpp")
                        {
                            f.SetSkipped();
                            continue;
                        }                            

                        _ParseFile(proj, f);
                        f.SetParsed();
                    }
                }
                reader.Close();
            }
        }

        void ExcludeAllUsedFiles()
        {
            foreach (SolutionFile sf in m_slnFiles)
            {
                m_allFiles.Remove(sf.FileName);
            }
        }

        void ExcludeIgnoredItems()
        {
            foreach (string s in m_ignoredFiles)
            {
                m_allFiles.Remove(s);
            }
        }

        void _ParseFile(ProjectInfo projInfo, SolutionFile slnFile)
        {
            string projPath = PathsHelper.RetrievePath(projInfo.ProjectPath);
            using (var reader = new StreamReader(slnFile.FileName))
            {                
                while (true)
                {
                    string s = reader.ReadLine();
                    if (s == null)
                        break;

                    if (!s.StartsWith("#include"))
                        continue;

                    // extracting include file name
                    int startPos = s.IndexOf('"');
                    if (startPos < 0)
                        startPos = s.IndexOf('<');

                    int endPos = s.LastIndexOf('"');
                    if (endPos < 0)
                        endPos = s.LastIndexOf('>');

                    string includeFileName = s.Substring(startPos + 1, endPos - startPos - 1);
                    string normalizedIncludeFileName = "";
                    bool isFileFound = false;
                    if (Path.IsPathRooted(includeFileName))
                    {
                        normalizedIncludeFileName = includeFileName;
                        if (File.Exists(normalizedIncludeFileName))
                            isFileFound = true;
                    }
                    else
                    {                        
                        string currFilePath = PathsHelper.RetrievePath(slnFile.FileName);
                        normalizedIncludeFileName = PathsHelper.MakeFullPathFromRelative(currFilePath, includeFileName);
                        if (File.Exists(normalizedIncludeFileName))
                            isFileFound = true;

                        if (!isFileFound)
                        {
                            normalizedIncludeFileName = PathsHelper.MakeFullPathFromRelativeEx(
                                projInfo.projectRelPaths, 
                                includeFileName);
                            if (File.Exists(normalizedIncludeFileName))
                                isFileFound = true;
                        }
                    }

                    if (!isFileFound)
                    {
                        projInfo.errorReport.AppendLine(String.Format("Project {0}. Unresolved #include \"{1}\" in file {2}", projInfo.projectName, includeFileName, slnFile.FileName));
                    }
                    else
                    {
                        InsertSolutionFile(projInfo, normalizedIncludeFileName, slnFile);
                    }
                }
            }
        }

        void InsertSolutionFile(ProjectInfo proj, string fileName, SolutionFile fileThatUseIt)
        {
            bool exists = false;
            SolutionFile slnFile = _AddSolutionFile(fileName, out exists);
            slnFile.AddProjectThatUseIt(proj);
            slnFile.AddFileThatUseIt(fileThatUseIt);            
        }

        bool _ParseProjectString(string projectString, ref ProjectInfo projInfo)
        {
            string hellGuid = "";
            string projName = "";
            string projPath = "";
            string projGuid = "";

            // projectString should looks like this
            // Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utils", "Utils", "{A1F25EAD-9DC8-410D-957B-F6A7E57636AD}"

            int startPos = 0, endPos = 0;
            
            startPos = projectString.IndexOf('"') + 1;
            endPos = projectString.IndexOf('"', startPos + 1);
            if (startPos < 0 || endPos < 0)
                return false;
            hellGuid = projectString.Substring(startPos, endPos - startPos);

            startPos = projectString.IndexOf('"', endPos + 1) + 1;
            endPos = projectString.IndexOf('"', startPos + 1);
            if (startPos < 0 || endPos < 0)
                return false;
            projName = projectString.Substring(startPos, endPos - startPos);
            if (projName.Length == 0)
                return false;

            startPos = projectString.IndexOf('"', endPos + 1) + 1;
            endPos = projectString.IndexOf('"', startPos + 1);
            if (startPos < 0 || endPos < 0)
                return false;
            projPath = projectString.Substring(startPos, endPos - startPos);

            startPos = projectString.IndexOf('"', endPos + 1) + 1;
            endPos = projectString.IndexOf('"', startPos + 1);
            if (startPos < 0 || endPos < 0)
                return false;
            projGuid = projectString.Substring(startPos, endPos - startPos);
            if (projGuid.Length == 0)
                return false;

            projInfo.projectName = projName;
            projInfo.ProjectPath = projPath;
            projInfo.projectGuid = projGuid;

            return true;
        }
    }
}
